package hu.vissy.yacap.parser;

import hu.vissy.notification.Notification;
import hu.vissy.notification.NotificationPool;
import hu.vissy.yacap.OptionSet;
import hu.vissy.yacap.exception.CommandLineParserException;
import hu.vissy.yacap.option.CommandLineOption;
import hu.vissy.yacap.option.OptionPolicy;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class ParserResult {

    private OptionSet optionSet;

    private Map<String, Object> values = new HashMap<String, Object>();

    private List<String> unnamedArguments = new ArrayList<>();
    private String[] internalArguments = new String[0];

    private NotificationPool messages = new NotificationPool();

    /**
     * Constructor.
     *
     * @param optionSet
     *            The option set the result is based on.
     */
    public ParserResult(OptionSet optionSet) {
        this.optionSet = optionSet;
    }

    /**
     * @return the optionSet
     */
    public OptionSet getOptionSet() {
        return optionSet;
    }

    /**
     * Sets an option value.
     *
     * @param key
     *            The key to set.
     * @param value
     *            The value to set.
     */
    void setValue(String key, Object value) {
        CommandLineOption<?> o = optionSet.findByLongForm(key);
        if (o == null) {
            throw new CommandLineParserException("Option with value key '" + key + "' was not found.");
        }

        values.put(o.getValueKey(), value);
    }

    /**
     * Sets an option value.
     *
     * @param key
     *            The option to set the value of.
     * @param value
     *            The value to set.
     */
    void setValue(CommandLineOption<?> option, Object value) {
        setValue(option.getValueKey(), value);
    }

    /**
     * Sets the internal (unprocessed) arguments.
     * <p>
     * All arguments after the double hyphen sign (--) regarded as internal
     * arguments and stored without parsing.
     * </p>
     *
     * @param args
     *            The arguments to set.
     */
    public void setInternalParameters(String[] args) {
        internalArguments = Arrays.copyOf(args, args.length);
    }

    /**
     * Sets the unnamed arguments.
     *
     * @param args
     *            The arguments to set.
     */
    public void setUnnamedParameters(String[] args) {
        unnamedArguments = Arrays.asList(args);
    }

    /*
     * (non-Javadoc)
     *
     * @see java.lang.Object#toString()
     */
    @Override
    public String toString() {
        return "ParserResult [values=" + values + ", internalArguments=" + internalArguments + ", unnamedArguments=" + unnamedArguments + "]";
    }

    public Map<String, Object> getValues() {
        return Collections.unmodifiableMap(values);
    }

    public List<String> getUnnamedParameters() {
        return Collections.unmodifiableList(unnamedArguments);
    }

    public String[] getInternalParameters() {
        return Arrays.copyOf(internalArguments, internalArguments.length);
    }

    void addMessage(Notification message) {
        messages.add(message);
    }

    public NotificationPool getMessagePool() {
        return messages;
    }

    public void fillOut() {
        for (CommandLineOption<?> o : optionSet.getOptions()) {
            String key = o.getValueKey();
            if (!values.containsKey(key)) {
                if (o.getDefaultOptionPolicy() == OptionPolicy.HIDDEN || o.getDefaultOptionPolicy() == OptionPolicy.OPTIONAL_AUTO) {
                    values.put(key, o.getDefaultValue());
                }
            }
        }
    }


    /**
     * Returns a parameter value as string.
     * <p>
     * Note that, in contrast with any other typed-getter implementations, this
     * function will not check option type and and will return the value of any
     * parameter.
     * </p>
     *
     * @param key
     *            The key of the searched parameter.
     * @return The value of the parameter or null if not specified.
     * @throws CommandLineParserException
     *             If the key is not registered in the associated option set.
     */
    public String getAsString(String key) {
        if (optionSet.findByValueKey(key) == null) {
            throw new CommandLineParserException("Unknown value key: " + key);
        }
        if (!values.containsKey(key)) {
            return null;
        }
        return values.get(key).toString();
    }



    /**
     * Checks the type and availability of the option.
     *
     * @param key
     *            The key of the searched parameter.
     *
     * @throws CommandLineParserException
     *             If the option associated with the key is not an integer or no
     *             option associated with the key.
     */
    private void checkOption(String key, Class<?> clazz) {
        CommandLineOption<?> o = optionSet.findByValueKey(key);
        if (o == null) {
            throw new CommandLineParserException("Unknown value key: " + key);
        }
        if (!clazz.isAssignableFrom(o.getDataType())) {
            throw new CommandLineParserException("Incompatible data type " + o.getDataType().getSimpleName() + " for key: " + key);
        }
    }

    /**
     * Returns a parameter value as integer.
     *
     * @param key
     *            The key of the searched parameter.
     * @return The value of the parameter or null if not specified.
     *
     * @throws CommandLineParserException
     *             If the option associated with the key is not an integer or no
     *             option associated with the key.
     */
    public Integer getAsInt(String key) {
        checkOption(key, Integer.class);
        return (Integer) values.get(key);
    }

    /**
     * Returns a parameter value as integer.
     *
     * @param key
     *            The key of the searched parameter.
     * @return The value of the parameter or null if not specified.
     *
     * @throws CommandLineParserException
     *             If the option associated with the key is not a long or no
     *             option associated with the key.
     */
    public Long getAsLong(String key) {
        checkOption(key, Long.class);
        return (Long) values.get(key);
    }

    /**
     * Returns a parameter value as boolean.
     *
     * @param key
     *            The key of the searched parameter.
     * @return The value of the parameter or null if not specified.
     *
     * @throws CommandLineParserException
     *             If the option associated with the key is not a boolean or no
     *             option associated with the key.
     */
    public Boolean getAsBoolean(String key) {
        checkOption(key, Boolean.class);
        return (Boolean) values.get(key);
    }

    /**
     * Returns a parameter value as double.
     *
     * @param key
     *            The key of the searched parameter.
     * @return The value of the parameter or null if not specified.
     *
     * @throws CommandLineParserException
     *             If the option associated with the key is not a double or no
     *             option associated with the key.
     */
    public Double getAsDouble(String key) {
        checkOption(key, Double.class);
        return (Double) values.get(key);
    }

    /**
     * Returns a parameter value as an enum.
     *
     * @param key
     *            The key of the searched parameter.
     * @param enumClass
     *            The class of the enum.
     * @return The value of the parameter or null if not specified.
     *
     * @throws CommandLineParserException
     *             If the option associated with the key is not an enum of the
     *             parameter type or no option associated with the key.
     */
    @SuppressWarnings("unchecked")
    public <E extends Enum<?>> E getAsEnum(String key, Class<? extends E> enumClass) {
        checkOption(key, enumClass);
        return (E) values.get(key);
    }

}
